package org.osami.gateway.watteco;

import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;

import org.apache.felix.ipojo.ComponentInstance;
import org.apache.felix.ipojo.Factory;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.ServiceProperty;
import org.apache.felix.ipojo.annotations.Validate;
import org.osami.gateway.watteco.interfaces.IW1NodeIPv6;
import org.osami.gateway.watteco.interfaces.NodeInfoIPv6;
import org.osgi.service.device.Constants;

@Component
@Provides
public class W1NodeIPv6 implements IW1NodeIPv6
{
    private static final Logger logger = Logger.getLogger(W1NodeIPv6.class.getName());

    private final static String[] HEX_DIGITS = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" };

    @ServiceProperty
    private String portName;

    private SerialPort port;

    private Set<String> plugs;

    private InputStream portIn;

    private OutputStream portOut;

    private short datagramID;

    private List<ComponentInstance> componentInstances;

    @Requires(filter = "(factory.name=org.osami.gateway.watteco.WattecoSmartplugIPv6)")
    Factory nodeFactory;

    private volatile boolean quit;
    private final long delay = 1500;
    private final long dutyCycle = 8;
    private final String instanceNamePrefix = "org.osami.gateway.watteco.WattecoSmartplugIPv6_";


    public W1NodeIPv6()
    {
        super();
        this.componentInstances = new ArrayList<ComponentInstance>();
    }

    public void noDriverFound() {
        logger.info("W1NodeIPv6.noDriverFound()");
    }

    @Validate
    private void start()
    {
        logger.info("Starting W1NodeIPv6");
        init();
    }

    @Invalidate
    private void stop()
    {
        logger.info("Stopping W1NodeIPv6");
        close();
    }

    /**
     * @param port
     * @throws NoSuchPortException
     * @throws PortInUseException
     * @throws UnsupportedCommOperationException
     * @throws IOException if communication can not be set up properly
     */
    public void init()
    {
        try {
            String actualPortName;
            actualPortName = System.getenv("OSAMI_WATTECO_IPV6_PORTNAME");
            if (actualPortName == null) {
                actualPortName = portName;
            }
            CommPortIdentifier portId = CommPortIdentifier.getPortIdentifier(actualPortName);
            this.port = (SerialPort) (portId.open("W1", 1500));
            this.port.setSerialPortParams(38400, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
            this.port.enableReceiveTimeout(1000);
            this.port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
            this.plugs = new TreeSet<String>();
            this.portIn = this.port.getInputStream();
            this.portOut = this.port.getOutputStream();
            this.datagramID = 0;
        }
        catch (Exception e) { // NoSuchPortException, PortInUseException, UnsupportedCommOperationException, IOException
            throw new RuntimeException(e);
        }
    }

    public void close()
    {
        this.port.close();
    }

    public Set<String> getPlugs()
    {
        return Collections.unmodifiableSet(this.plugs);
    }

    private String getResponse(long delay)
    {
        Timer timer = new Timer(delay);
        timer.start();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        while (true)
        {
            if (timer.hasExpired()) break;
            int incoming = 0;
            try
            {
                incoming = this.portIn.read();
            }
            catch (IOException e)
            {
                logger.severe("Exception while reading");
                break;
            }
            if (incoming == -1)
            {
                logger.fine("End of Stream");
                break;
            }
            bos.write(incoming);
        }
        String result = "";
        try
        {
            result = bos.toString("US-ASCII");
        }
        catch (UnsupportedEncodingException e)
        {
            e.printStackTrace();
        }
        return result;
    }

    private String parseResponse (String moduleAddress, String expectedPacketBeginning, String resp)
    {
        if (!resp.equals(""))
        {
            String expectedContents = "$UDP" + moduleAddress.toLowerCase() + "%" + expectedPacketBeginning;
            System.out.println(expectedContents);
            int idx = resp.indexOf(expectedContents);
            if (idx != -1) {
                resp = resp.substring(expectedContents.length()).trim();
                if (resp.endsWith(";")) {
                    return resp.substring(0, resp.length() - 1);
                }
            }
        }
        else
        {
            logger.info("No response from module.");
        }
        return null;
    }

    public String validateAddress(String moduleAddress)
    {
        moduleAddress = moduleAddress.toUpperCase();
        if (moduleAddress.matches("[0-9A-F]{4}:[0-9A-F]{4}:[0-9A-F]{4}:[0-9A-F]{4}:[0-9A-F]{4}:[0-9A-F]{4}:[0-9A-F]{4}:[0-9A-F]{4}"))
        {
            return moduleAddress;
        }
        else
        {
            logger.severe("Improper module address \"" + moduleAddress + "\", skipping");
            return null;
        }
    }

    public synchronized String getW1ID() throws IOException
    {
        logger.fine("Sending '?'");
        this.portOut.write("$?;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(250);
        System.out.println(response);
        logger.fine("Finished");

        return response;
    }

    public synchronized String getW1IP() throws IOException
    {
        logger.fine("Sending 'MYIP'");
        this.portOut.write("$MYIP;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(1000);
        String result = null;
        String[] tokens = response.split("  ");
        if (tokens.length == 2 && tokens[0].equals("MYIP:") && validateAddress(tokens[1]) != null) {
            result = tokens[1];
        }

        logger.fine("Finished");
        return result;
    }

    public synchronized List<NodeInfoIPv6> getNodesInfo() throws IOException
    {
        logger.fine("Sending SN");
        this.portOut.write("$SN;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(5000);
        System.out.println(response);
        logger.fine("Finished");

        // Parse response
        List<NodeInfoIPv6> result = new LinkedList<NodeInfoIPv6>();
        BufferedReader br = new BufferedReader(new StringReader(response));

        while (true)
        {

            String line = br.readLine();
            if (line == null) break;
            String[] tokens = line.split("  ");
            if (tokens.length != 2) continue;
            NodeInfoIPv6.Type type = NodeInfoIPv6.Type.PLUG;
            if (tokens[0].equals("MYIP:")) {
                // Don't store this special node.
                type = NodeInfoIPv6.Type.W1;
            }
            if (tokens[0].equals("NODE:")) {
                result.add(new NodeInfoIPv6(tokens[1].trim(), type));
            }
        }
        return result;
    }

    public synchronized void viewHelp() throws IOException
    {
        logger.fine("Sending H");
        this.portOut.write("$H;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(1000);
        System.out.println(response);
        logger.fine("Finished");
    }

    public synchronized void sendReset() throws IOException
    {
        logger.fine("Sending RESET");
        this.portOut.write("$RESET;".getBytes("US-ASCII"));
        this.portOut.flush();

        String response = this.getResponse(5000);
        System.out.println(response);
        logger.fine("Finished");
    }

    public synchronized void discoverSmartplugs() throws Exception {

        List<NodeInfoIPv6> nodes = this.getNodesInfo();
        for (NodeInfoIPv6 nodeInfo : nodes)
        {
            if (NodeInfoIPv6.Type.PLUG.equals(nodeInfo.getNodeType())) {
                String plugId = nodeInfo.getID();
                plugId = validateAddress(plugId);

                if (plugId != null) {
                    boolean found = false;
                    String expectedInstanceName = instanceNamePrefix + plugId;

                    for (ComponentInstance instance : componentInstances) {
                        String instanceName = instance.getInstanceName();
                        if (instanceName.equals(expectedInstanceName)) {
                            logger.info("Found existing WattecoSmartplugIPv6 instance for " + plugId + ", not creating one");
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        logger.info("Creating WattecoSmartplugIPv6 instance for " + plugId + ".");

                        Dictionary<String, Object> requiresFilters = new Hashtable<String, Object>();
                        requiresFilters.put("power", "(&(sense.type=Power)(device.id=" + plugId + "))");

                        Dictionary<String, Object> instanceConfiguration = new Hashtable<String, Object>();
                        instanceConfiguration.put(Constants.DEVICE_SERIAL, plugId);
                        instanceConfiguration.put("instance.name", expectedInstanceName);
                        instanceConfiguration.put("requires.filters", requiresFilters);

                        // Call factory for instance creation
                        ComponentInstance ci = nodeFactory.createComponentInstance(instanceConfiguration);
                        componentInstances.add(ci);
                        logger.info("WattecoSmartplugIPv6 instance for " + plugId + " created.");

                        // Reset measurements.
                        startMeasurements(plugId);
                    }
                }
            }
        }
    }

    private void setSwitchState(String switchID, String command) throws IOException
    {
        switchID = validateAddress(switchID);
        if (switchID != null)
        {
            String datagramIP = "$UDP" + switchID + "%11500006" + command;

            logger.fine(datagramIP);

            this.portOut.write(datagramIP.getBytes("US-ASCII"));
            this.portOut.flush();

            logger.fine("Finished");
        }
    }

    public synchronized void switchOn(String switchID) throws IOException
    {
        setSwitchState(switchID, "01;");
    }

    public synchronized void switchOff(String switchID) throws IOException
    {
        setSwitchState(switchID, "00;");
    }

    public synchronized void toggleSwitch(String switchID) throws IOException
    {
        setSwitchState(switchID, "02;");
    }

    public synchronized void setSwitchState(String switchID, boolean state) throws IOException
    {
        if (state) {
            setSwitchState(switchID, "01;");
        }
        else {
            setSwitchState(switchID, "00;");
        }
    }

    public synchronized int getSwitchState(String switchID) throws IOException
    {
        int retval = -1;
        switchID = validateAddress(switchID);
        if (switchID != null)
        {
            String datagramIP = "$UDP" + switchID + "%110000060000;";

            logger.fine(datagramIP);

            this.portOut.write(datagramIP.getBytes("US-ASCII"));
            this.portOut.flush();

            String response = this.getResponse(5000);
            System.out.println(response);
            response = parseResponse(switchID, "1101000600000010", response);
            if (response != null) {
                // response should be 00 or 01.
                retval = Integer.parseInt(response);
            }

            System.out.println();
            logger.fine("Finished");
        }
        return retval;
    }

    public synchronized void startMeasurements(String meterID) throws IOException
    {
        meterID = validateAddress(meterID);
        if (meterID != null)
        {
            String datagramIP = "$UDP" + meterID + "%1150070200;";

            logger.fine(datagramIP);

            this.portOut.write(datagramIP.getBytes("US-ASCII"));
            this.portOut.flush();

            logger.fine("Finished");
        }
    }

    public synchronized int getMeasurement(String meterID) throws IOException
    {
        int retval = -1;
        meterID = validateAddress(meterID);
        if (meterID != null)
        {
            String datagramIP = "$UDP" + meterID + "%110007020000;";

            logger.fine(datagramIP);

            this.portOut.write(datagramIP.getBytes("US-ASCII"));
            this.portOut.flush();

            String response = this.getResponse(5000);
            System.out.println(response);
            response = parseResponse(meterID, "1101070200000025", response);
            if (response != null) {
                if (response.length() == 16) {
                    // response should be made of 3 fields:
                    // 24-bit: reactive power;
                    // 24-bit: active power;
                    // 16-bit: number of measurements.
                    String activePower = response.substring(6, 12);
                    String nbSec = response.substring(12, 16);
                    int nbSecInt = Integer.parseInt(nbSec, 16);
                    if (nbSecInt != 0) {
                        return (Integer.parseInt(activePower, 16) / nbSecInt);
                    }
                    else {
                        return 0;
                    }
                }
            }
            System.out.println();
            logger.fine("Finished");
        }
        return retval;
    }
}
